﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Windows.Input;
using System.Xml;
using Microsoft.Practices.Prism.Interactivity.InteractionRequest;
using Microsoft.Practices.Prism.PubSubEvents;
using Mitosoft.Definitions.Notifications;
using Mitosoft.Definitions.Services;
using Prism.Common.ApplicationEvents;
using Prism.Common.Interfaces;
using Microsoft.Practices.Prism.Commands;
using Mitosoft.Definitions.ViewModels;
using TextFileReaderModule.Manager;
using TextFileReaderModule.Notifications;

namespace TextFileReaderModule.ViewModels
{
	/// <summary>
	/// Text file reading view model.
	/// Used by the view.
	/// </summary>
	// ReSharper disable ClassNeverInstantiated.Global
	public class TextFileReaderViewModel : BaseViewModel
	// ReSharper restore ClassNeverInstantiated.Global
	{ 
		#region fields
		
		/// <summary>
		/// Represents <see cref="SelectLogFileCommand"/>
		/// </summary>
		protected ICommand _selectLogFileCommand;

		/// <summary>
		/// Represents <see cref="SaveCommentsCommand"/>
		/// </summary>
		protected ICommand _saveCommentsCommand;

    /// <summary>
    /// Represents <see cref="_shutDownCommand"/>
    /// </summary>
	  protected ICommand _shutDownCommand;

		/// <summary>
		/// Represents <see cref="GotoLogLineCommand"/>
		/// </summary>
		protected ICommand _gotoLogLineCommand;
		
		/// <summary>
		/// Represents the event aggregator used to find defined application events.
		/// </summary>
		protected readonly IEventAggregator _eventAggregator;

		/// <summary>
		/// Represents <see cref="ScrollSelectedTextLineIntoView"/>
		/// </summary>
		protected bool _scrollSelectedTextLineIntoView;

		/// <summary>
		/// Represents <see cref="SelectedTextLine"/>
		/// </summary>
		protected object _selectedTextLine;

    /// <summary>
    /// Represents the service handling application wide stuff.
    /// </summary>
	  protected IApplicationManagerService _applicationManagerService;

    /// <summary>
    /// Represents <see cref="MultipleFilesDataManager"/>
    /// </summary>
    protected MultipleFilesDataManager _multipleFilesDataManager = new MultipleFilesDataManager();

    /// <summary>
    /// Represents the directory service.
    /// </summary>
	  protected IDirectoryService _directoryService;

		#endregion

		#region constants

		/// <summary>
		/// Size of cache used to store line positions.
		/// </summary>
		protected const uint LINE_POSITIONS_CACHE_SIZE = 1024;

		/// <summary>
		/// Size of cache used to store line contents.
		/// </summary>
		protected const uint LINE_CONTENT_CACHE_SIZE = 1024;

		protected readonly string _genericDataBlockItemEventProviderId = Guid.NewGuid().ToString();

		#endregion

	  #region constructor, destructor

	  /// <summary>
	  /// Creates an <see cref="TextFileReaderViewModel"/> instance.
	  /// </summary>
	  /// <param name="eventAggregator">Aggregates events.</param>
	  /// <param name="applicationManagerService">Represents the service handling application wide stuff.</param>
	  /// <param name="directoryService">Represents the directory service.</param>
	  public TextFileReaderViewModel(
      IEventAggregator eventAggregator,
      IApplicationManagerService applicationManagerService,
      IDirectoryService directoryService)
	  {
      _eventAggregator = eventAggregator;
		  MultipleFilesDataManager.CommentService.NeedsToBeSaved.ValueChanged += CommentServiceNeedsToBeSavedValueChanged;

		  _eventAggregator.GetEvent<GenericDataBlockItemEvent>().Subscribe(
			  OnGenericDataBlockItemEvent, ThreadOption.BackgroundThread);

		  GotoLineNumberDialogRequest = new InteractionRequest<GotoLineNumberDialogNotification>();
      FilesSelectionDialogRequest = new InteractionRequest<FilesSelectionDialogNotification>();
      SaveCommentsDialogRequest = new InteractionRequest<SaveCommentsDialogNotification>();

	    _applicationManagerService = applicationManagerService;
      _applicationManagerService.ShutDownApplicationCommand.RegisterCommand(ShutDownCommand);

	    _directoryService = directoryService;
	  }

    /// <summary>
    /// Destructor. Will run only if Dispose has not been called.
    /// </summary>
	  ~TextFileReaderViewModel()
    {
      Dispose(false);
    }

	  #endregion

		#region properties

    /// <summary>
    /// Represents the manager handling multiple files as data.
    /// </summary>
    public MultipleFilesDataManager MultipleFilesDataManager
	  {
	    get
	    {
	      return _multipleFilesDataManager;
	    }
	  }

	  /// <summary>
		/// Request used to initiate a dialog where the user may enter the line number he wants to see.
		/// </summary>
		public InteractionRequest<GotoLineNumberDialogNotification> GotoLineNumberDialogRequest { get; private set; }

    /// <summary>
    /// Request used to initiate a dialog where the user is able to select the files to be displayed.
    /// </summary>
    public InteractionRequest<FilesSelectionDialogNotification> FilesSelectionDialogRequest { get; private set; }

    /// <summary>
    /// Request used to initiate a dialog where the user chooses whether to save comments.
    /// </summary>
    public InteractionRequest<SaveCommentsDialogNotification> SaveCommentsDialogRequest { get; private set; }

		/// <summary>
		/// Gets or sets the selected text line.
		/// </summary>
		public object SelectedTextLine
		{
			get
			{
				return _selectedTextLine;
			}
			set
			{
				_selectedTextLine = value;

        HandlePropertyChanged();
			}
		}

		/// <summary>
		/// Gets or sets whether the currently selected text line should be scrolled into the view.
		/// </summary>
		public bool ScrollSelectedTextLineIntoView
		{
			get
			{
				return _scrollSelectedTextLineIntoView;
			}
			protected set
			{
				_scrollSelectedTextLineIntoView = value;

        HandlePropertyChanged();
			}
		}

		/// <summary>
		/// Represents the command used to select the log file.
		/// Used by the view.
		/// </summary>
		// ReSharper disable UnusedMember.Global
		public ICommand SelectLogFileCommand
		// ReSharper restore UnusedMember.Global
		{
			get
			{
				return 
					_selectLogFileCommand ?? 
						(_selectLogFileCommand = 
							new DelegateCommand<object>(
								OnSelectLogFileCommandExecuted));
			}
		}

		/// <summary>
		/// Represents the command used to save persistently the comments.
		/// </summary>
		// ReSharper disable UnusedMember.Global
		public ICommand SaveCommentsCommand
		// ReSharper restore UnusedMember.Global
		{
			get
			{
				return 
					_saveCommentsCommand ?? 
						(_saveCommentsCommand =
				           new DelegateCommand(
					           OnSaveCommentsCommandExecuted, CanExecuteSaveCommentsCommand));
			}
		}

    /// <summary>
    /// Represents the command used to shut down this module.
    /// </summary>
	  public ICommand ShutDownCommand
	  {
	    get
	    {
	      return 
          _shutDownCommand ?? 
            (_shutDownCommand = 
              new DelegateCommand<CancelEventArgs>(
                  OnShutDownCommandExecuted, CanExecuteShutDownCommand));
	    }
	  }

	  /// <summary>
		/// Gets whether <see cref="SaveCommentsCommand"/> should be enabled.
		/// </summary>
		// ReSharper disable UnusedMember.Global
		public bool SaveCommentsCommandIsEnabled
		// ReSharper restore UnusedMember.Global
		{
			get
			{
        return MultipleFilesDataManager.CommentService != null && MultipleFilesDataManager.CommentService.NeedsToBeSaved.Value;
			}
		}

		/// <summary>
		/// Represents the command used to go to a specific log line being identified by the number.
		/// Used by the view.
		/// </summary>
		// ReSharper disable UnusedMember.Global
		public ICommand GotoLogLineCommand
		// ReSharper restore UnusedMember.Global
		{
			get
			{
				return 
					_gotoLogLineCommand ??
						(_gotoLogLineCommand = 
							new DelegateCommand<object>(
								OnGotoLogLineCommandExecuted));
			}
		}

		#endregion

		#region methods

    /// <summary>
    /// Disposes this object.
    /// </summary>
    /// <param name="isDisposing">Represents whether this object is disposing.</param>
    protected override void Dispose(bool isDisposing)
    {
      if (_isDisposed) return;

      if (isDisposing)
      {
        #region dispose managed resources

        MultipleFilesDataManager.CommentService.NeedsToBeSaved.ValueChanged -= CommentServiceNeedsToBeSavedValueChanged;
        _eventAggregator.GetEvent<GenericDataBlockItemEvent>().Unsubscribe(OnGenericDataBlockItemEvent);
        _applicationManagerService.ShutDownApplicationCommand.UnregisterCommand(ShutDownCommand);
        MultipleFilesDataManager.Dispose();

        #endregion
      }

      #region clean up unmanaged resources

      #endregion

      _isDisposed = true;
    }

    /// <summary>
    /// NeedsToBeSaved from the comment service has changed.
    /// </summary>
    /// <param name="sender">Not used.</param>
    /// <param name="needsToBeSaved">New NeedsToBeSaved value</param>
    protected void CommentServiceNeedsToBeSavedValueChanged(object sender, bool needsToBeSaved)
    {
      // ReSharper disable once ExplicitCallerInfoArgument
      HandlePropertyChanged("SaveCommentsCommandIsEnabled");
      ((DelegateCommand)SaveCommentsCommand).RaiseCanExecuteChanged();
      ((DelegateCommand<CancelEventArgs>)ShutDownCommand).RaiseCanExecuteChanged();
    }

    /// <summary>
    /// Returns whether <see cref="ShutDownCommand"/> can be executed.
    /// </summary>
    /// <returns></returns>
    protected bool CanExecuteShutDownCommand(CancelEventArgs cancelEventArgs)
    {
      return true;
    }

    /// <summary>
    /// Gets whether <see cref="SaveCommentsCommand"/> can be executed.
    /// </summary>
    /// <returns>True if <see cref="SaveCommentsCommand"/> can be executed.</returns>
    protected bool CanExecuteSaveCommentsCommand()
    {
      return SaveCommentsCommandIsEnabled;
    }

		/// <summary>
		/// Executes <see cref="SaveCommentsCommand"/>
		/// </summary>
		protected void OnSaveCommentsCommandExecuted()
		{
      if (   MultipleFilesDataManager.CommentService == null 
          || MultipleFilesDataManager.CommentService.NeedsToBeSaved == null 
          || !MultipleFilesDataManager.CommentService.NeedsToBeSaved.Value
          || MultipleFilesDataManager.Files == null
          || !MultipleFilesDataManager.Files.Any())
		    return;

		  foreach (KeyValuePair<int, string> orderAndFileNameKVP in MultipleFilesDataManager.Files)
		  {
        var serializer = new DataContractSerializer(typeof(IEnumerable<KeyValuePair<ulong, string>>));
		    using (
		      FileStream fileStream = File.Open(string.Format("{0}{1}", orderAndFileNameKVP.Value, ".comments.xml"),
		        FileMode.Create))
		    {
          serializer.WriteObject(
            fileStream,
            MultipleFilesDataManager.CommentService.Comments(
              orderAndFileNameKVP.Key, 
              MultipleFilesDataManager.CachedCollectionBlockDataProvider.GetDataProviderOffset(orderAndFileNameKVP.Key)));
		    }
		  }

      MultipleFilesDataManager.CommentService.NeedsToBeSaved.Value = false;
		}

    /// <summary>
    /// <see cref="ShutDownCommand"/> shall be executed.
    /// </summary>
    protected void OnShutDownCommandExecuted(CancelEventArgs cancelEventArgs)
    {
      if (!MultipleFilesDataManager.CommentService.NeedsToBeSaved.Value)
      {
        cancelEventArgs.Cancel = false;
        return;
      }

      ShowSaveCommentsDialog(cancelEventArgs);
    }

    /// <summary>
    /// Shows the dialog asking the user whether he wants to save the comments.
    /// </summary>
    /// <param name="cancelEventArgs">Used to determine whether the dialog was cancelled.</param>
	  protected void ShowSaveCommentsDialog(CancelEventArgs cancelEventArgs)
	  {
	    var notification = new SaveCommentsDialogNotification {Title = "Save comments dialog"};

	    SaveCommentsDialogRequest.Raise(
	      notification,
	      returned =>
	      {
	        if (returned == null || returned.CommentsShouldBeSaved == null)
	        {
	          // Dialog was cancelled
	          cancelEventArgs.Cancel = true;
	          return;
	        }

	        if (!(bool) returned.CommentsShouldBeSaved)
	        {
	          // user dont want to save the comments
	          cancelEventArgs.Cancel = false;
	          return;
	        }

	        // User want to save the comments
	        OnSaveCommentsCommandExecuted();
	        cancelEventArgs.Cancel = false;
	      }
	    );
	  }

	  /// <summary>
		/// Listen on the generic data block events.
		/// </summary>
		/// <param name="arguments">Arguments used to recognize whether a request is being send and 
		/// to know which data is requested.</param>
		protected void OnGenericDataBlockItemEvent( GenericDataBlockItemEventArguments arguments )
		{
			if (arguments == null) 
				return;	// no information available at all

			if (string.IsNullOrEmpty(arguments.RequestingId) || !string.IsNullOrEmpty(arguments.ProvidingId))
				return;	// expecting an available request Id and no providing Id.

			if (arguments.NumberOfDataItems == 0)
				return;	// No data requested.

			if (MultipleFilesDataManager.CachedCollectionBlockDataProvider == null)
			{
				arguments.DataBlockItems = null; // Nothing to provide
			}
			else
			{
        MultipleFilesDataManager.CachedCollectionBlockDataProvider.BlockIListRequests = arguments.EvaluationIsRunning;
        arguments.DataBlockItems = MultipleFilesDataManager.CachedCollectionBlockDataProvider.Values(arguments.StartingDataId, arguments.NumberOfDataItems).ToList();
			}

			#region publish result

			arguments.ProvidingId = _genericDataBlockItemEventProviderId;
			_eventAggregator.GetEvent<GenericDataBlockItemEvent>().Publish(arguments);

			#endregion
		}

		/// <summary>
		/// <see cref="SelectLogFileCommand"/> has been executed.
		/// </summary>
		/// <param name="obj">Not used.</param>
		protected void OnSelectLogFileCommandExecuted(object obj)
    {
      #region check whether comments must be saved ...

      if (MultipleFilesDataManager.CommentService.NeedsToBeSaved.Value)
      {
        var cancelEventArgs = new CancelEventArgs();
        
        ShowSaveCommentsDialog(cancelEventArgs);

        if (cancelEventArgs.Cancel)
          return;
      }

      #endregion

      var notification = new FilesSelectionDialogNotification
		  {
		    Title = "Select text files...",
		    DirectoryService = _directoryService,
        EventAggregator = _eventAggregator,
        InitialZoomFactor = _applicationManagerService == null ? 1.0 : _applicationManagerService.ZoomFactor
		  };

		  FilesSelectionDialogRequest.Raise(
		    notification,
		    returned =>
		    {
		      if (returned == null || !returned.Confirmed || returned.FileNames == null)
		        return; // The user cancelled the dialog or didn`t select files

          var selectedFiles = new SortedDictionary<int, string>();

		      foreach (KeyValuePair<int, IOViewModel> orderAndFileKVP in returned.FileNames)
		        selectedFiles.Add(orderAndFileKVP.Key,orderAndFileKVP.Value.FullPath);

		      MultipleFilesDataManager.Files = selectedFiles;

          LoadCommentsFromFile();
		    }
		  );
		}

    /// <summary>
    /// Loads the comments from file.
    /// </summary>
	  protected void LoadCommentsFromFile()
	  {
	    if (MultipleFilesDataManager.Files == null || !MultipleFilesDataManager.Files.Any())
        return;

      foreach (KeyValuePair<int, string> orderAndFileNameKVP in MultipleFilesDataManager.Files)
	    {
        string nameOfCommentsFile = string.Format("{0}{1}", orderAndFileNameKVP.Value, ".comments.xml");

        if (!File.Exists(nameOfCommentsFile)) continue;

        var serializer = new DataContractSerializer(typeof(IEnumerable<KeyValuePair<ulong, string>>));
        using (FileStream fs = File.Open(nameOfCommentsFile, FileMode.Open))
        {

          var comments = serializer.ReadObject(fs) as IEnumerable<KeyValuePair<ulong, string>>;
          if (comments == null) continue;

          ulong offset = MultipleFilesDataManager.CachedCollectionBlockDataProvider.GetDataProviderOffset(orderAndFileNameKVP.Key);

          foreach (KeyValuePair<ulong, string> idCommentKVP in comments)
          {
            MultipleFilesDataManager.CommentService.Add(idCommentKVP.Key + offset, idCommentKVP.Value, orderAndFileNameKVP.Key);
          }
        }
	    }

      MultipleFilesDataManager.CommentService.NeedsToBeSaved.Value = false;
	  }

		/// <summary>
		/// <see cref="GotoLogLineCommand"/> has been executed.
		/// </summary>
		/// <param name="obj">Not used.</param>
		protected void OnGotoLogLineCommandExecuted( object obj )
    {
      #region test code 
      /* Used to get a specific item.
      var selectedTextLine = CachedBlockDataProvider == null ? null : CachedBlockDataProvider[15000];
      SelectedTextLine = selectedTextLine;
      ScrollSelectedTextLineIntoView = true;
      ScrollSelectedTextLineIntoView = false;	// Reset
      */
      #endregion

      var notification = new GotoLineNumberDialogNotification {Title = "Goto line number..."};

		  GotoLineNumberDialogRequest.Raise( 
        notification,
		    returned =>
		    {
		      if (returned == null || !returned.Confirmed || returned.GotoLineNumber == null) 
            return; // The user cancelled the dialog or didn`t entered a line number

		      SelectedTextLine = null;
          var selectedTextLine = MultipleFilesDataManager.CachedCollectionBlockDataProvider == null ? null : MultipleFilesDataManager.CachedCollectionBlockDataProvider[(int)returned.GotoLineNumber]; 
          
		      if (selectedTextLine != null) SelectedTextLine = selectedTextLine;
		      ScrollSelectedTextLineIntoView = true;
		      ScrollSelectedTextLineIntoView = false;	// Reset
		    }
      );
		}

		#endregion
	}
}
